iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Modern Web

在Vibe Coding 時代一起來做沒有AI感的漂亮網站吧!系列 第 17

Hold On! GSAP你先借我滑鼠控制權滑別的東西

  • 分享至 

  • xImage
  •  

嗨咿,我是 illumi,昨天我們用GSAP Timeline + ScrollTrigger 來用滑鼠播放動畫,但播到一半我想控制其他東西怎麼辦?

外層一個大範圍的沈浸式動畫滑到一半時,要「暫停」大動畫,接著讓內層小動畫(例如 Carousel、遊戲區塊)接手控制。

這時候就需要靠 GSAP Timeline + ScrollTrigger 來「分段控制動畫」。
Yes

STEP 1 HTML

import ChildAnime from "你的路徑";
//外層動畫
<div ref={mainRef}>
//其他元素
  <ChildAnime isCarouselMode={isCarouselMode}/>
</div>

STEP 2 建立外層 Timeline 與 ScrollTrigger

首先,我們用一個大動畫來主宰整個頁面的沉浸感。這裡我直接用 mainRef 來當外層元素。

import { useRef, useState, useEffect } from "react";
const mainRef = useRef<HTMLDivElement>(null);
const [isCarouselMode, setIsCarouselMode] = useState(false);

useGSAP(() => {
  if (!mainRef.current) return;
  const tl = gsap.timeline();
  tl.to(mainRef.current, {
    keyframes: [
      { scale: 1, opacity: 0 },
      { scale: 1, opacity: 1 },
      { scale: 10, opacity: 1 },
      { scale: 10, opacity: 1 },
      { scale: 5, opacity: 1 },
    ],
    duration: 1,
    scrollTrigger: {
      trigger: mainRef.current,
      start: "top top",
      end: "+=1000%",
      scrub: 1,
      pin: true,
      pinSpacing: true,
      onUpdate: (self) => {
        const progress = self.progress;
        if (progress >= 0.4 && progress <= 0.6) {
          setIsCarouselMode(true);  // 暫停大動畫,交給內層
        } else {
          setIsCarouselMode(false); // 外層動畫繼續跑
        }
      },
    },
  });
});

屬性逐一解釋:

  • trigger:決定誰是捲動的觸發點。這裡是 mainRef,所以動畫跟這個元素掛鉤。
  • start: "top top":當外層元素頂部 碰到視窗頂部 時,動畫開始。
  • end: "+=1000%":動畫持續的捲動範圍。這裡是比螢幕高度多 10 倍的距離。
  • scrub: 1:動畫跟隨滾動,並有 1 秒緩動。數字代表補間的時間。
  • pin: true:固定這個元素,不讓它捲走。這是「沈浸式」的關鍵。
  • pinSpacing: true:是否在 pin 的地方保留空間,避免後面元素「跳起來」。
  • onUpdate:每次捲動更新時都會執行。這裡我用 progress(0 到 1 的進度條)來判斷何時進入內層動畫。

STEP 2 啟用內層動畫的「條件」

在外層進度進到 0.4 到 0.6 之間時,代表大動畫暫停,我們就啟用內層動畫。

if (progress >= 0.4 && progress <= 0.6) {
  setIsCarouselMode(true);
} else {
  setIsCarouselMode(false);
}

這樣一來,我們就能用 isCarouselMode 這個狀態去決定內層要不要動。


STEP 3 寫內層 Carousel 動畫

當外層暫停後,我們希望內層(例如輪播、遊戲機)接手。這裡我用一個 Carousel 例子。

useGSAP(() => {
  if (!isCarouselMode || !carouselRef.current || isTriggered) return;
  setIsTriggered(true);

  // 內層淡入效果
  gsap.to(".title-icon", {
    opacity: 1,
    duration: 1,
    ease: "power1.inOut",
  });

  // 內層捲動控制
  gsap.to(".slide", {
    scrollTrigger: {
      trigger: carouselRef.current,
      start: "top top",
      end: "+=250%",
      scrub: 1,
      onUpdate: (self) => {
        setSlideIndex(Math.max(0, Math.floor(self.progress * 5) - 1));
      },
    },
  });
}, { dependencies: [isCarouselMode] });

<div ref={carouselRef}>
內層動畫
</div>

屬性解釋(內層部分)

  • trigger: carouselRef.current:內層捲動範圍以 Carousel 區域為準。
  • start / end:內層動畫會跑 250% 的捲動距離。
  • scrub: 1:一樣是跟隨捲動,並保留平滑感。
  • onUpdate:隨著捲動更新 slideIndex,讓 Carousel 滑到不同頁。

為什麼內層不能用swiper /shadcn /其他寫滑動翻頁的輪播圖套件:

1. 滾動驅動 vs 手勢驅動

  • GSAP ScrollTrigger 的邏輯是:

    內層動畫完全跟隨外層的「頁面滾動進度」。

    → 使用者滾動多少,動畫就走多少,progress 會從 0 → 1 線性對應。

  • Swiper / shadcn 的邏輯是:

    它們是 手勢 / 按鈕驅動,每次都是一頁一頁切換,並不是「進度條」驅動。

    → 這些套件沒辦法被 ScrollTrigger 綁定到「滾動百分比」上。

用 Swiper,它的翻頁行為和 ScrollTrigger 的進度不同步,外層停在 0.45 進度時,Swiper 可能卡在第 2 頁,但 GSAP 需要一個「數字進度」去算 slide index。這兩個系統會打架。


2. Pin + Scroll 範圍的問題

  • 外層動畫已經 pin 住畫面,代表內層也要在同一段 scroll 範圍內工作。
  • Swiper/shadcn 類的套件,通常是 自成一個手勢容器,它吃到滑動事件就直接阻止瀏覽器滾動,導致 ScrollTrigger 的「進度」完全卡死。

外層在等 scroll progress,內層卻在搶滑動事件,這樣兩個動畫會互相搶控制權。

耶依~醬就可以做出來了! 我們明天再見~


上一篇
GSAP Timeline(tl)——做出沉浸式網頁效果,進入你的世界
下一篇
當物理學介入GSAP掉落動畫:Matter.js
系列文
在Vibe Coding 時代一起來做沒有AI感的漂亮網站吧!21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言